驗證分為兩種,登入權限驗證以及角色驗證
舉例說明:我們將 API 分為三種情境
不需要登入
呼叫 API 時因為完全不需要驗證,所以我們不會設定 Guard
登入不需要權限 @UseGuards(AuthGuard)
我們設定某些 API 是需要登入後才能呼叫的,也就是 API 的 headers 必須帶著有效的 Token ,Guard 會進行 Token 解析,通過便能呼叫此 API ,如果失敗就會回傳 400 或者 401 的狀態
登入後需要角色驗證 @UseGuards(AuthGuard, RolesGuard)
最後便是角色權限的驗證,像是某些 API 只能由角色是Admin 來呼叫,這時我們就會多一個 RolesGuard 的驗證,@UseGuards 是有先後順序的,我們通常會先驗證 AuthGuard 再來驗證 RolesGuard ,如果 RolesGuard 驗證失敗就會回傳 403 狀態
在 NestJs 中有一個主題是在說明 Authentication,有很多種做法,我這邊會針對 GraphQL 來說明如何實作 Auth 的驗證
NestJs 中提供了幾個套件能夠驗證
@nestjs/common
提供的介面,我們需要實作此介面@nestjs/passport
提供的Class,我們會透過擴充的方式來使用它我會使用 NestJs 提供的套件 @nestjs/jwt 來產生 Token 以及解析 Token
@Module({
imports: [
JwtModule.register({
secret: 'test', // 為了測試方便先直接明文寫
signOptions: { expiresIn: '1h' }, // 有效時長
}),
]
})
在登入後使用 jwtService 產生一個有效的 Token ,裡面放了 username 以及角色,方便我後續做角色驗證
@Mutation(() => String)
async login (
@Args() userArgs: UserArgs
) {
const user = await this.userService.findUser(userArgs);
const accessToken = this.jwtService.sign({
role: user.role,
username: user.username
});
return accessToken;
}
將 request (req) object 傳給 context
@Module({
imports: [
GraphQLModule.forRoot({
context: ({ req }) => ({ req })
}),
UsersModule
]
})
export class AppModule {}
取得 Request headers 中的 Token ,透過在 usersService 實作好的 validateToken 來解析 Token ,將解析出來的 User 回傳,最後再塞回去 req 中
import {
ExecutionContext,
Injectable,
UnauthorizedException,
BadRequestException
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
import { UsersService } from './users.service';
@Injectable()
// export class GqlAuthGuard implements CanActivate {
export class GqlAuthGuard extends AuthGuard('jwt') {
constructor(private readonly usersService: UsersService) {
super() // 實作 CanActivate 不需要
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = this.getRequest(context);
const authHeader = req.headers.authorization as string;
if (!authHeader) {
throw new BadRequestException('Authorization header not found.');
}
const { isValid, user } = await this.usersService.validateToken(authHeader)
if (isValid) {
req.user = user;
return true;
}
throw new UnauthorizedException('Token not valid');
}
}
能在 req 中取得剛剛放進去的 user ,再將它回傳出去,我們就能夠在使用 @CurrentUser
時取得 User
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
接著在到需要驗證的 API 上加上需要的 Decorator
@UseGuards(GqlAuthGuard)
@Query(() => TaskConnection)
async doneTasks(
@CurrentUser() user: User,
@Args() taskArgs: TaskArgs
) {
const tasks = await this.taskService.queryTasks(taskArgs, TaskStatus.DONE );
const taskCount = await this.taskService.taskCount(taskArgs, TaskStatus.DONE);
return { tasks, taskCount};
}
如果有跨 Module 記得要將 UserService Export ,因為我們在 AuthGuard 有使用 UserService
角色驗證相對來說就簡單一些
使用 Reflector 可以取得 Decorator 的內容, super-roles
是我自定義的 Decorator ,等等會說明該如何建立
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private readonly reflector: Reflector
) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]>('super-roles', context.getHandler());
const { role } = GqlExecutionContext.create(context).getContext().req.user;
if (roles.indexOf(role) === -1) return false;
return true;
}
}
import { SetMetadata } from '@nestjs/common';
export const SuperRoles = (...roles: string[]) => SetMetadata('super-roles', roles);
接著到 API 上設定 Role
在 @UseGuards 上加上 RolesGuard,並使用 @SuperRoles
設定角色,RolesGuard 就能取得角色來做驗證
@UseGuards(GqlAuthGuard, RolesGuard)
@SuperRoles('vip')
@Query(() => TaskConnection)
async doneTasks(
@CurrentUser() user: User,
@Args() taskArgs: TaskArgs
) {
const tasks = await this.taskService.queryTasks(taskArgs, TaskStatus.DONE );
const taskCount = await this.taskService.taskCount(taskArgs, TaskStatus.DONE);
return { tasks, taskCount};
}
@Mutation(() => Task)
async createTask(@Args('taskData') taskData: TaskInput) {
const task = await this.taskService.createTask(taskData);
return task;
}
@UseGuards(RolesGuard)
@Query(() => TaskConnection)
async doneTasks() {}
@UseGuards(RolesGuard)
@Resolver()
export class TasksResolver {}
main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
app.module.ts
providers: [{
provide: APP_GUARD,
useClass: RolesGuard,
}]